/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2012 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.mail.imap.protocol;

import java.io.*;
import java.util.*;
import java.text.*;
import java.lang.reflect.*;
import java.util.logging.Level;

import javax.mail.*;
import javax.mail.internet.*;
import javax.mail.search.*;

import com.sun.mail.util.*;
import com.sun.mail.iap.*;
import com.sun.mail.auth.Ntlm;
import com.sun.mail.imap.ACL;
import com.sun.mail.imap.Rights;
import com.sun.mail.imap.AppendUID;
import com.sun.mail.imap.SortTerm;

/**
 * This class extends the iap.Protocol object and implements IMAP
 * semantics. In general, there is a method corresponding to each
 * IMAP protocol command. The typical implementation issues the
 * appropriate protocol command, collects all responses, processes
 * those responses that are specific to this command and then
 * dispatches the rest (the unsolicited ones) to the dispatcher
 * using the <code>notifyResponseHandlers(r)</code>.
 * 
 * @author John Mani
 * @author Bill Shannon
 */

public class IMAPProtocol extends Protocol {

	private boolean connected = false; // did constructor succeed?
	private boolean rev1 = false; // REV1 server ?
	private boolean noauthdebug = true; // hide auth info in debug output
	private boolean authenticated; // authenticated?
	// WARNING: authenticated may be set to true in superclass
	// constructor, don't initialize it here.

	private Map capabilities;
	// WARNING: capabilities may be initialized as a result of superclass
	// constructor, don't initialize it here.
	private List authmechs;
	// WARNING: authmechs may be initialized as a result of superclass
	// constructor, don't initialize it here.

	protected SearchSequence searchSequence;
	protected String[] searchCharsets; // array of search charsets

	private String name;
	private SaslAuthenticator saslAuthenticator; // if SASL is being used

	private ByteArray ba; // a buffer for fetchBody

	private static final byte[] CRLF = { (byte) '\r', (byte) '\n' };

	private static final FetchItem[] fetchItems = {};

	/**
	 * Constructor.
	 * Opens a connection to the given host at given port.
	 * 
	 * @param host
	 *            host to connect to
	 * @param port
	 *            portnumber to connect to
	 * @param debug
	 *            debug mode
	 * @param props
	 *            Properties object used by this protocol
	 */
	public IMAPProtocol(String name, String host, int port, Properties props, boolean isSSL, MailLogger logger) throws IOException, ProtocolException {
		super(host, port, props, "mail." + name, isSSL, logger);

		try {
			this.name = name;
			noauthdebug = !PropUtil.getBooleanProperty(props, "mail.debug.auth", false);

			if (capabilities == null)
				capability();

			if (hasCapability("IMAP4rev1"))
				rev1 = true;

			searchCharsets = new String[2]; // 2, for now.
			searchCharsets[0] = "UTF-8";
			searchCharsets[1] = MimeUtility.mimeCharset(MimeUtility.getDefaultJavaCharset());

			connected = true; // must be last statement in constructor
		} finally {
			/*
			 * If we get here because an exception was thrown, we need
			 * to disconnect to avoid leaving a connected socket that
			 * no one will be able to use because this object was never
			 * completely constructed.
			 */
			if (!connected)
				disconnect();
		}
	}

	/**
	 * Return an array of FetchItem objects describing the
	 * FETCH items supported by this protocol. Subclasses may
	 * override this method to combine their FetchItems with
	 * the FetchItems returned by the superclass.
	 * 
	 * @since JavaMail 1.4.6
	 */
	public FetchItem[] getFetchItems() {
		return fetchItems;
	}

	/**
	 * CAPABILITY command.
	 * 
	 * @see "RFC2060, section 6.1.1"
	 */
	public void capability() throws ProtocolException {
		// Check CAPABILITY
		Response[] r = command("CAPABILITY", null);

		if (!r[r.length - 1].isOK())
			throw new ProtocolException(r[r.length - 1].toString());

		capabilities = new HashMap(10);
		authmechs = new ArrayList(5);
		for (int i = 0, len = r.length; i < len; i++) {
			if (!(r[i] instanceof IMAPResponse))
				continue;

			IMAPResponse ir = (IMAPResponse) r[i];

			// Handle *all* untagged CAPABILITY responses.
			// Though the spec seemingly states that only
			// one CAPABILITY response string is allowed (6.1.1),
			// some server vendors claim otherwise.
			if (ir.keyEquals("CAPABILITY"))
				parseCapabilities(ir);
		}
	}

	/**
	 * If the response contains a CAPABILITY response code, extract
	 * it and save the capabilities.
	 */
	protected void setCapabilities(Response r) {
		byte b;
		while ((b = r.readByte()) > 0 && b != (byte) '[')
			;
		if (b == 0)
			return;
		String s;
		s = r.readAtom();
		if (!s.equalsIgnoreCase("CAPABILITY"))
			return;
		capabilities = new HashMap(10);
		authmechs = new ArrayList(5);
		parseCapabilities(r);
	}

	/**
	 * Parse the capabilities from a CAPABILITY response or from
	 * a CAPABILITY response code attached to (e.g.) an OK response.
	 */
	protected void parseCapabilities(Response r) {
		String s;
		while ((s = r.readAtom(']')) != null) {
			if (s.length() == 0) {
				if (r.peekByte() == (byte) ']')
					break;
				/*
				 * Probably found something here that's not an atom.
				 * Rather than loop forever or fail completely, we'll
				 * try to skip this bogus capability. This is known
				 * to happen with:
				 * Netscape Messaging Server 4.03 (built Apr 27 1999)
				 * that returns:
				 * * CAPABILITY * CAPABILITY IMAP4 IMAP4rev1 ...
				 * The "*" in the middle of the capability list causes
				 * us to loop forever here.
				 */
				r.skipToken();
			} else {
				capabilities.put(s.toUpperCase(Locale.ENGLISH), s);
				if (s.regionMatches(true, 0, "AUTH=", 0, 5)) {
					authmechs.add(s.substring(5));
					if (logger.isLoggable(Level.FINE))
						logger.fine("AUTH: " + s.substring(5));
				}
			}
		}
	}

	/**
	 * Check the greeting when first connecting; look for PREAUTH response.
	 */
	protected void processGreeting(Response r) throws ProtocolException {
		super.processGreeting(r); // check if it's BAD
		if (r.isOK()) { // check if it's OK
			setCapabilities(r);
			return;
		}
		// only other choice is PREAUTH
		IMAPResponse ir = (IMAPResponse) r;
		if (ir.keyEquals("PREAUTH")) {
			authenticated = true;
			setCapabilities(r);
		} else
			throw new ConnectionException(this, r);
	}

	/**
	 * Returns <code>true</code> if the connection has been authenticated,
	 * either due to a successful login, or due to a PREAUTH greeting response.
	 */
	public boolean isAuthenticated() {
		return authenticated;
	}

	/**
	 * Returns <code>true</code> if this is a IMAP4rev1 server
	 */
	public boolean isREV1() {
		return rev1;
	}

	/**
	 * Returns whether this Protocol supports non-synchronizing literals.
	 */
	protected boolean supportsNonSyncLiterals() {
		return hasCapability("LITERAL+");
	}

	/**
	 * Read a response from the server.
	 */
	public Response readResponse() throws IOException, ProtocolException {
		// assert Thread.holdsLock(this);
		// can't assert because it's called from constructor
		IMAPResponse r = new IMAPResponse(this);
		if (r.keyEquals("FETCH"))
			r = new FetchResponse(r, getFetchItems());
		return r;
	}

	/**
	 * Check whether the given capability is supported by
	 * this server. Returns <code>true</code> if so, otherwise
	 * returns false.
	 */
	public boolean hasCapability(String c) {
		if (c.endsWith("*")) {
			c = c.substring(0, c.length() - 1).toUpperCase(Locale.ENGLISH);
			Iterator it = capabilities.keySet().iterator();
			while (it.hasNext()) {
				if (((String) it.next()).startsWith(c))
					return true;
			}
			return false;
		}
		return capabilities.containsKey(c.toUpperCase(Locale.ENGLISH));
	}

	/**
	 * Return the map of capabilities returned by the server.
	 * 
	 * @since JavaMail 1.4.1
	 */
	public Map getCapabilities() {
		return capabilities;
	}

	/**
	 * Close socket connection.
	 * 
	 * This method just makes the Protocol.disconnect() method
	 * public.
	 */
	public void disconnect() {
		super.disconnect();
		authenticated = false; // just in case
	}

	/**
	 * The NOOP command.
	 * 
	 * @see "RFC2060, section 6.1.2"
	 */
	public void noop() throws ProtocolException {
		logger.fine("IMAPProtocol noop");
		simpleCommand("NOOP", null);
	}

	/**
	 * LOGOUT Command.
	 * 
	 * @see "RFC2060, section 6.1.3"
	 */
	public void logout() throws ProtocolException {
		try {
			Response[] r = command("LOGOUT", null);

			authenticated = false;
			// dispatch any unsolicited responses.
			// NOTE that the BYE response is dispatched here as well
			notifyResponseHandlers(r);
		} finally {
			disconnect();
		}
	}

	/**
	 * LOGIN Command.
	 * 
	 * @see "RFC2060, section 6.2.2"
	 */
	public void login(String u, String p) throws ProtocolException {
		Argument args = new Argument();
		args.writeString(u);
		args.writeString(p);

		Response[] r = null;
		try {
			if (noauthdebug && isTracing()) {
				logger.fine("LOGIN command trace suppressed");
				suspendTracing();
			}
			r = command("LOGIN", args);
		} finally {
			resumeTracing();
		}

		// dispatch untagged responses
		notifyResponseHandlers(r);

		// Handle result of this command
		if (noauthdebug && isTracing())
			logger.fine("LOGIN command result: " + r[r.length - 1]);
		handleResult(r[r.length - 1]);
		// If the response includes a CAPABILITY response code, process it
		setCapabilities(r[r.length - 1]);
		// if we get this far without an exception, we're authenticated
		authenticated = true;
	}

	/**
	 * The AUTHENTICATE command with AUTH=LOGIN authenticate scheme
	 * 
	 * @see "RFC2060, section 6.2.1"
	 */
	public synchronized void authlogin(String u, String p) throws ProtocolException {
		Vector v = new Vector();
		String tag = null;
		Response r = null;
		boolean done = false;

		try {

			if (noauthdebug && isTracing()) {
				logger.fine("AUTHENTICATE LOGIN command trace suppressed");
				suspendTracing();
			}

			try {
				tag = writeCommand("AUTHENTICATE LOGIN", null);
			} catch (Exception ex) {
				// Convert this into a BYE response
				r = Response.byeResponse(ex);
				done = true;
			}

			OutputStream os = getOutputStream(); // stream to IMAP server

			/*
			 * Wrap a BASE64Encoder around a ByteArrayOutputstream
			 * to craft b64 encoded username and password strings
			 * 
			 * Note that the encoded bytes should be sent "as-is" to the
			 * server, *not* as literals or quoted-strings.
			 * 
			 * Also note that unlike the B64 definition in MIME, CRLFs
			 * should *not* be inserted during the encoding process. So, I
			 * use Integer.MAX_VALUE (0x7fffffff (> 1G)) as the bytesPerLine,
			 * which should be sufficiently large !
			 * 
			 * Finally, format the line in a buffer so it can be sent as
			 * a single packet, to avoid triggering a bug in SUN's SIMS 2.0
			 * server caused by patch 105346.
			 */

			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE);
			boolean first = true;

			while (!done) { // loop till we are done
				try {
					r = readResponse();
					if (r.isContinuation()) {
						// Server challenge ..
						String s;
						if (first) { // Send encoded username
							s = u;
							first = false;
						} else
							// Send encoded password
							s = p;

						// obtain b64 encoded bytes
						b64os.write(ASCIIUtility.getBytes(s));
						b64os.flush(); // complete the encoding

						bos.write(CRLF); // CRLF termination
						os.write(bos.toByteArray()); // write out line
						os.flush(); // flush the stream
						bos.reset(); // reset buffer
					} else if (r.isTagged() && r.getTag().equals(tag))
						// Ah, our tagged response
						done = true;
					else if (r.isBYE()) // outta here
						done = true;
					else
						// hmm .. unsolicited response here ?!
						v.addElement(r);
				} catch (Exception ioex) {
					// convert this into a BYE response
					r = Response.byeResponse(ioex);
					done = true;
				}
			}

		} finally {
			resumeTracing();
		}

		/*
		 * Dispatch untagged responses.
		 * NOTE: in our current upper level IMAP classes, we add the
		 * responseHandler to the Protocol object only *after* the
		 * connection has been authenticated. So, for now, the below
		 * code really ends up being just a no-op.
		 */
		Response[] responses = new Response[v.size()];
		v.copyInto(responses);
		notifyResponseHandlers(responses);

		// Handle the final OK, NO, BAD or BYE response
		if (noauthdebug && isTracing())
			logger.fine("AUTHENTICATE LOGIN command result: " + r);
		handleResult(r);
		// If the response includes a CAPABILITY response code, process it
		setCapabilities(r);
		// if we get this far without an exception, we're authenticated
		authenticated = true;
	}

	/**
	 * The AUTHENTICATE command with AUTH=PLAIN authentication scheme.
	 * This is based heavly on the {@link #authlogin} method.
	 * 
	 * @param authzid
	 *            the authorization id
	 * @param u
	 *            the username
	 * @param p
	 *            the password
	 * @throws ProtocolException
	 *             as thrown by {@link Protocol#handleResult}.
	 * @see "RFC3501, section 6.2.2"
	 * @see "RFC2595, section 6"
	 * @since JavaMail 1.3.2
	 */
	public synchronized void authplain(String authzid, String u, String p) throws ProtocolException {
		Vector v = new Vector();
		String tag = null;
		Response r = null;
		boolean done = false;

		try {

			if (noauthdebug && isTracing()) {
				logger.fine("AUTHENTICATE PLAIN command trace suppressed");
				suspendTracing();
			}

			try {
				tag = writeCommand("AUTHENTICATE PLAIN", null);
			} catch (Exception ex) {
				// Convert this into a BYE response
				r = Response.byeResponse(ex);
				done = true;
			}

			OutputStream os = getOutputStream(); // stream to IMAP server

			/*
			 * Wrap a BASE64Encoder around a ByteArrayOutputstream
			 * to craft b64 encoded username and password strings
			 * 
			 * Note that the encoded bytes should be sent "as-is" to the
			 * server, *not* as literals or quoted-strings.
			 * 
			 * Also note that unlike the B64 definition in MIME, CRLFs
			 * should *not* be inserted during the encoding process. So, I
			 * use Integer.MAX_VALUE (0x7fffffff (> 1G)) as the bytesPerLine,
			 * which should be sufficiently large !
			 * 
			 * Finally, format the line in a buffer so it can be sent as
			 * a single packet, to avoid triggering a bug in SUN's SIMS 2.0
			 * server caused by patch 105346.
			 */

			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE);

			while (!done) { // loop till we are done
				try {
					r = readResponse();
					if (r.isContinuation()) {
						// Server challenge ..
						final String nullByte = "\0";
						String s = (authzid == null ? "" : authzid) + nullByte + u + nullByte + p;

						// obtain b64 encoded bytes
						b64os.write(ASCIIUtility.getBytes(s));
						b64os.flush(); // complete the encoding

						bos.write(CRLF); // CRLF termination
						os.write(bos.toByteArray()); // write out line
						os.flush(); // flush the stream
						bos.reset(); // reset buffer
					} else if (r.isTagged() && r.getTag().equals(tag))
						// Ah, our tagged response
						done = true;
					else if (r.isBYE()) // outta here
						done = true;
					else
						// hmm .. unsolicited response here ?!
						v.addElement(r);
				} catch (Exception ioex) {
					// convert this into a BYE response
					r = Response.byeResponse(ioex);
					done = true;
				}
			}

		} finally {
			resumeTracing();
		}

		/*
		 * Dispatch untagged responses.
		 * NOTE: in our current upper level IMAP classes, we add the
		 * responseHandler to the Protocol object only *after* the
		 * connection has been authenticated. So, for now, the below
		 * code really ends up being just a no-op.
		 */
		Response[] responses = new Response[v.size()];
		v.copyInto(responses);
		notifyResponseHandlers(responses);

		// Handle the final OK, NO, BAD or BYE response
		if (noauthdebug && isTracing())
			logger.fine("AUTHENTICATE PLAIN command result: " + r);
		handleResult(r);
		// If the response includes a CAPABILITY response code, process it
		setCapabilities(r);
		// if we get this far without an exception, we're authenticated
		authenticated = true;
	}

	/**
	 * The AUTHENTICATE command with AUTH=NTLM authentication scheme.
	 * This is based heavly on the {@link #authlogin} method.
	 * 
	 * @param authzid
	 *            the authorization id
	 * @param u
	 *            the username
	 * @param p
	 *            the password
	 * @throws ProtocolException
	 *             as thrown by {@link Protocol#handleResult}.
	 * @see "RFC3501, section 6.2.2"
	 * @see "RFC2595, section 6"
	 * @since JavaMail 1.4.3
	 */
	public synchronized void authntlm(String authzid, String u, String p) throws ProtocolException {
		Vector v = new Vector();
		String tag = null;
		Response r = null;
		boolean done = false;

		String type1Msg = null;
		int flags = PropUtil.getIntProperty(props, "mail." + name + ".auth.ntlm.flags", 0);
		String domain = props.getProperty("mail." + name + ".auth.ntlm.domain", "");
		Ntlm ntlm = new Ntlm(domain, getLocalHost(), u, p, logger);

		try {

			if (noauthdebug && isTracing()) {
				logger.fine("AUTHENTICATE NTLM command trace suppressed");
				suspendTracing();
			}

			try {
				tag = writeCommand("AUTHENTICATE NTLM", null);
			} catch (Exception ex) {
				// Convert this into a BYE response
				r = Response.byeResponse(ex);
				done = true;
			}

			OutputStream os = getOutputStream(); // stream to IMAP server
			boolean first = true;

			while (!done) { // loop till we are done
				try {
					r = readResponse();
					if (r.isContinuation()) {
						// Server challenge ..
						String s;
						if (first) {
							s = ntlm.generateType1Msg(flags);
							first = false;
						} else {
							s = ntlm.generateType3Msg(r.getRest());
						}

						os.write(ASCIIUtility.getBytes(s));
						os.write(CRLF); // CRLF termination
						os.flush(); // flush the stream
					} else if (r.isTagged() && r.getTag().equals(tag))
						// Ah, our tagged response
						done = true;
					else if (r.isBYE()) // outta here
						done = true;
					else
						// hmm .. unsolicited response here ?!
						v.addElement(r);
				} catch (Exception ioex) {
					// convert this into a BYE response
					r = Response.byeResponse(ioex);
					done = true;
				}
			}

		} finally {
			resumeTracing();
		}

		/*
		 * Dispatch untagged responses.
		 * NOTE: in our current upper level IMAP classes, we add the
		 * responseHandler to the Protocol object only *after* the
		 * connection has been authenticated. So, for now, the below
		 * code really ends up being just a no-op.
		 */
		Response[] responses = new Response[v.size()];
		v.copyInto(responses);
		notifyResponseHandlers(responses);

		// Handle the final OK, NO, BAD or BYE response
		if (noauthdebug && isTracing())
			logger.fine("AUTHENTICATE NTLM command result: " + r);
		handleResult(r);
		// If the response includes a CAPABILITY response code, process it
		setCapabilities(r);
		// if we get this far without an exception, we're authenticated
		authenticated = true;
	}

	/**
	 * SASL-based login.
	 */
	public void sasllogin(String[] allowed, String realm, String authzid, String u, String p) throws ProtocolException {
		if (saslAuthenticator == null) {
			try {
				Class sac = Class.forName("com.sun.mail.imap.protocol.IMAPSaslAuthenticator");
				Constructor c = sac.getConstructor(new Class[] { IMAPProtocol.class, String.class, Properties.class, Boolean.TYPE, PrintStream.class, String.class });
				saslAuthenticator = (SaslAuthenticator) c.newInstance(new Object[] { this, name, props, logger, host });
			} catch (Exception ex) {
				logger.log(Level.FINE, "Can't load SASL authenticator", ex);
				// probably because we're running on a system without SASL
				return; // not authenticated, try without SASL
			}
		}

		// were any allowed mechanisms specified?
		List v;
		if (allowed != null && allowed.length > 0) {
			// remove anything not supported by the server
			v = new ArrayList(allowed.length);
			for (int i = 0; i < allowed.length; i++)
				if (authmechs.contains(allowed[i])) // XXX - case must match
					v.add(allowed[i]);
		} else {
			// everything is allowed
			v = authmechs;
		}
		String[] mechs = (String[]) v.toArray(new String[v.size()]);

		try {

			if (noauthdebug && isTracing()) {
				logger.fine("SASL authentication command trace suppressed");
				suspendTracing();
			}

			if (saslAuthenticator.authenticate(mechs, realm, authzid, u, p)) {
				if (noauthdebug && isTracing())
					logger.fine("SASL authentication succeeded");
				authenticated = true;
			} else {
				if (noauthdebug && isTracing())
					logger.fine("SASL authentication failed");
			}
		} finally {
			resumeTracing();
		}
	}

	// XXX - for IMAPSaslAuthenticator access to protected method
	OutputStream getIMAPOutputStream() {
		return getOutputStream();
	}

	/**
	 * PROXYAUTH Command.
	 * 
	 * @see "Netscape/iPlanet/SunONE Messaging Server extension"
	 */
	public void proxyauth(String u) throws ProtocolException {
		Argument args = new Argument();
		args.writeString(u);

		simpleCommand("PROXYAUTH", args);
	}

	/**
	 * ID Command, for Yahoo! Mail IMAP server.
	 * 
	 * See <A HREF=
	 * "http://en.wikipedia.org/wiki/Yahoo%21_Mail#Free_IMAP_and_SMTPs_access">
	 * http://en.wikipedia.org/wiki/Yahoo%21_Mail#Free_IMAP_and_SMTPs_access</A>
	 * 
	 * @since JavaMail 1.4.4
	 */
	public void id(String guid) throws ProtocolException {
		/*
		 * XXX - need to be able to write a string instead
		 * of an astring for the following to work.
		 * Argument garg = new Argument();
		 * garg.writeString("GUID");
		 * garg.writeString(guid);
		 * Argument args = new Argument();
		 * args.writeArgument(garg);
		 * simpleCommand("ID", args);
		 */
		simpleCommand("ID (\"GUID\" \"" + guid + "\")", null);
	}

	/**
	 * STARTTLS Command.
	 * 
	 * @see "RFC3501, section 6.2.1"
	 */
	public void startTLS() throws ProtocolException {
		try {
			super.startTLS("STARTTLS");
		} catch (ProtocolException pex) {
			logger.log(Level.FINE, "STARTTLS ProtocolException", pex);
			// ProtocolException just means the command wasn't recognized,
			// or failed. This should never happen if we check the
			// CAPABILITY first.
			throw pex;
		} catch (Exception ex) {
			logger.log(Level.FINE, "STARTTLS Exception", ex);
			// any other exception means we have to shut down the connection
			// generate an artificial BYE response and disconnect
			Response[] r = { Response.byeResponse(ex) };
			notifyResponseHandlers(r);
			disconnect();
			throw new ProtocolException("STARTTLS failure", ex);
		}
	}

	/**
	 * SELECT Command.
	 * 
	 * @see "RFC2060, section 6.3.1"
	 */
	public MailboxInfo select(String mbox) throws ProtocolException {
		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);

		Argument args = new Argument();
		args.writeString(mbox);

		Response[] r = command("SELECT", args);

		// Note that MailboxInfo also removes those responses
		// it knows about
		MailboxInfo minfo = new MailboxInfo(r);

		// dispatch any remaining untagged responses
		notifyResponseHandlers(r);

		Response response = r[r.length - 1];

		if (response.isOK()) { // command succesful
			if (response.toString().indexOf("READ-ONLY") != -1)
				minfo.mode = Folder.READ_ONLY;
			else
				minfo.mode = Folder.READ_WRITE;
		}

		handleResult(response);
		return minfo;
	}

	/**
	 * EXAMINE Command.
	 * 
	 * @see "RFC2060, section 6.3.2"
	 */
	public MailboxInfo examine(String mbox) throws ProtocolException {
		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);

		Argument args = new Argument();
		args.writeString(mbox);

		Response[] r = command("EXAMINE", args);

		// Note that MailboxInfo also removes those responses
		// it knows about
		MailboxInfo minfo = new MailboxInfo(r);
		minfo.mode = Folder.READ_ONLY; // Obviously

		// dispatch any remaining untagged responses
		notifyResponseHandlers(r);

		handleResult(r[r.length - 1]);
		return minfo;
	}

	/**
	 * UNSELECT Command.
	 * 
	 * @see "RFC 3691"
	 * @since JavaMail 1.4.4
	 */
	public void unselect() throws ProtocolException {
		if (!hasCapability("UNSELECT"))
			throw new BadCommandException("UNSELECT not supported");
		simpleCommand("UNSELECT", null);
	}

	/**
	 * STATUS Command.
	 * 
	 * @see "RFC2060, section 6.3.10"
	 */
	public Status status(String mbox, String[] items) throws ProtocolException {
		if (!isREV1() && !hasCapability("IMAP4SUNVERSION"))
			// STATUS is rev1 only, however the non-rev1 SIMS2.0
			// does support this.
			throw new BadCommandException("STATUS not supported");

		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);

		Argument args = new Argument();
		args.writeString(mbox);

		Argument itemArgs = new Argument();
		if (items == null)
			items = Status.standardItems;

		for (int i = 0, len = items.length; i < len; i++)
			itemArgs.writeAtom(items[i]);
		args.writeArgument(itemArgs);

		Response[] r = command("STATUS", args);

		Status status = null;
		Response response = r[r.length - 1];

		// Grab all STATUS responses
		if (response.isOK()) { // command succesful
			for (int i = 0, len = r.length; i < len; i++) {
				if (!(r[i] instanceof IMAPResponse))
					continue;

				IMAPResponse ir = (IMAPResponse) r[i];
				if (ir.keyEquals("STATUS")) {
					if (status == null)
						status = new Status(ir);
					else
						// collect 'em all
						Status.add(status, new Status(ir));
					r[i] = null;
				}
			}
		}

		// dispatch remaining untagged responses
		notifyResponseHandlers(r);
		handleResult(response);
		return status;
	}

	/**
	 * CREATE Command.
	 * 
	 * @see "RFC2060, section 6.3.3"
	 */
	public void create(String mbox) throws ProtocolException {
		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);

		Argument args = new Argument();
		args.writeString(mbox);

		simpleCommand("CREATE", args);
	}

	/**
	 * DELETE Command.
	 * 
	 * @see "RFC2060, section 6.3.4"
	 */
	public void delete(String mbox) throws ProtocolException {
		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);

		Argument args = new Argument();
		args.writeString(mbox);

		simpleCommand("DELETE", args);
	}

	/**
	 * RENAME Command.
	 * 
	 * @see "RFC2060, section 6.3.5"
	 */
	public void rename(String o, String n) throws ProtocolException {
		// encode the mbox as per RFC2060
		o = BASE64MailboxEncoder.encode(o);
		n = BASE64MailboxEncoder.encode(n);

		Argument args = new Argument();
		args.writeString(o);
		args.writeString(n);

		simpleCommand("RENAME", args);
	}

	/**
	 * SUBSCRIBE Command.
	 * 
	 * @see "RFC2060, section 6.3.6"
	 */
	public void subscribe(String mbox) throws ProtocolException {
		Argument args = new Argument();
		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);
		args.writeString(mbox);

		simpleCommand("SUBSCRIBE", args);
	}

	/**
	 * UNSUBSCRIBE Command.
	 * 
	 * @see "RFC2060, section 6.3.7"
	 */
	public void unsubscribe(String mbox) throws ProtocolException {
		Argument args = new Argument();
		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);
		args.writeString(mbox);

		simpleCommand("UNSUBSCRIBE", args);
	}

	/**
	 * LIST Command.
	 * 
	 * @see "RFC2060, section 6.3.8"
	 */
	public ListInfo[] list(String ref, String pattern) throws ProtocolException {
		return doList("LIST", ref, pattern);
	}

	/**
	 * LSUB Command.
	 * 
	 * @see "RFC2060, section 6.3.9"
	 */
	public ListInfo[] lsub(String ref, String pattern) throws ProtocolException {
		return doList("LSUB", ref, pattern);
	}

	/**
	 * Execute the specified LIST-like command (e.g., "LIST" or "LSUB"),
	 * using the reference and pattern.
	 * 
	 * @since JavaMail 1.4.6
	 */
	protected ListInfo[] doList(String cmd, String ref, String pat) throws ProtocolException {
		// encode the mbox as per RFC2060
		ref = BASE64MailboxEncoder.encode(ref);
		pat = BASE64MailboxEncoder.encode(pat);

		Argument args = new Argument();
		args.writeString(ref);
		args.writeString(pat);

		Response[] r = command(cmd, args);

		ListInfo[] linfo = null;
		Response response = r[r.length - 1];

		if (response.isOK()) { // command succesful
			Vector v = new Vector(1);
			for (int i = 0, len = r.length; i < len; i++) {
				if (!(r[i] instanceof IMAPResponse))
					continue;

				IMAPResponse ir = (IMAPResponse) r[i];
				if (ir.keyEquals(cmd)) {
					v.addElement(new ListInfo(ir));
					r[i] = null;
				}
			}
			if (v.size() > 0) {
				linfo = new ListInfo[v.size()];
				v.copyInto(linfo);
			}
		}

		// Dispatch remaining untagged responses
		notifyResponseHandlers(r);
		handleResult(response);
		return linfo;
	}

	/**
	 * APPEND Command.
	 * 
	 * @see "RFC2060, section 6.3.11"
	 */
	public void append(String mbox, Flags f, Date d, Literal data) throws ProtocolException {
		appenduid(mbox, f, d, data, false); // ignore return value
	}

	/**
	 * APPEND Command, return uid from APPENDUID response code.
	 * 
	 * @see "RFC2060, section 6.3.11"
	 */
	public AppendUID appenduid(String mbox, Flags f, Date d, Literal data) throws ProtocolException {
		return appenduid(mbox, f, d, data, true);
	}

	public AppendUID appenduid(String mbox, Flags f, Date d, Literal data, boolean uid) throws ProtocolException {
		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);

		Argument args = new Argument();
		args.writeString(mbox);

		if (f != null) { // set Flags in appended message
			// can't set the \Recent flag in APPEND
			if (f.contains(Flags.Flag.RECENT)) {
				f = new Flags(f); // copy, don't modify orig
				f.remove(Flags.Flag.RECENT); // remove RECENT from copy
			}

			/*
			 * HACK ALERT: We want the flag_list to be written out
			 * without any checking/processing of the bytes in it. If
			 * I use writeString(), the flag_list will end up being
			 * quoted since it contains "illegal" characters. So I
			 * am depending on implementation knowledge that writeAtom()
			 * does not do any checking/processing - it just writes out
			 * the bytes. What we really need is a writeFoo() that just
			 * dumps out its argument.
			 */
			args.writeAtom(createFlagList(f));
		}
		if (d != null) // set INTERNALDATE in appended message
			args.writeString(INTERNALDATE.format(d));

		args.writeBytes(data);

		Response[] r = command("APPEND", args);

		// dispatch untagged responses
		notifyResponseHandlers(r);

		// Handle result of this command
		handleResult(r[r.length - 1]);

		if (uid)
			return getAppendUID(r[r.length - 1]);
		else
			return null;
	}

	/**
	 * If the response contains an APPENDUID response code, extract
	 * it and return an AppendUID object with the information.
	 */
	private AppendUID getAppendUID(Response r) {
		if (!r.isOK())
			return null;
		byte b;
		while ((b = r.readByte()) > 0 && b != (byte) '[')
			;
		if (b == 0)
			return null;
		String s;
		s = r.readAtom();
		if (!s.equalsIgnoreCase("APPENDUID"))
			return null;

		long uidvalidity = r.readLong();
		long uid = r.readLong();
		return new AppendUID(uidvalidity, uid);
	}

	/**
	 * CHECK Command.
	 * 
	 * @see "RFC2060, section 6.4.1"
	 */
	public void check() throws ProtocolException {
		simpleCommand("CHECK", null);
	}

	/**
	 * CLOSE Command.
	 * 
	 * @see "RFC2060, section 6.4.2"
	 */
	public void close() throws ProtocolException {
		simpleCommand("CLOSE", null);
	}

	/**
	 * EXPUNGE Command.
	 * 
	 * @see "RFC2060, section 6.4.3"
	 */
	public void expunge() throws ProtocolException {
		simpleCommand("EXPUNGE", null);
	}

	/**
	 * UID EXPUNGE Command.
	 * 
	 * @see "RFC2359, section 4.1"
	 */
	public void uidexpunge(UIDSet[] set) throws ProtocolException {
		if (!hasCapability("UIDPLUS"))
			throw new BadCommandException("UID EXPUNGE not supported");
		simpleCommand("UID EXPUNGE " + UIDSet.toString(set), null);
	}

	/**
	 * Fetch the BODYSTRUCTURE of the specified message.
	 */
	public BODYSTRUCTURE fetchBodyStructure(int msgno) throws ProtocolException {
		Response[] r = fetch(msgno, "BODYSTRUCTURE");
		notifyResponseHandlers(r);

		Response response = r[r.length - 1];
		if (response.isOK())
			return (BODYSTRUCTURE) FetchResponse.getItem(r, msgno, BODYSTRUCTURE.class);
		else if (response.isNO())
			return null;
		else {
			handleResult(response);
			return null;
		}
	}

	/**
	 * Fetch given BODY section, without marking the message
	 * as SEEN.
	 */
	public BODY peekBody(int msgno, String section) throws ProtocolException {
		return fetchBody(msgno, section, true);
	}

	/**
	 * Fetch given BODY section.
	 */
	public BODY fetchBody(int msgno, String section) throws ProtocolException {
		return fetchBody(msgno, section, false);
	}

	protected BODY fetchBody(int msgno, String section, boolean peek) throws ProtocolException {
		Response[] r;

		if (peek)
			r = fetch(msgno, "BODY.PEEK[" + (section == null ? "]" : section + "]"));
		else
			r = fetch(msgno, "BODY[" + (section == null ? "]" : section + "]"));

		notifyResponseHandlers(r);

		Response response = r[r.length - 1];
		if (response.isOK())
			return (BODY) FetchResponse.getItem(r, msgno, BODY.class);
		else if (response.isNO())
			return null;
		else {
			handleResult(response);
			return null;
		}
	}

	/**
	 * Partial FETCH of given BODY section, without setting SEEN flag.
	 */
	public BODY peekBody(int msgno, String section, int start, int size) throws ProtocolException {
		return fetchBody(msgno, section, start, size, true, null);
	}

	/**
	 * Partial FETCH of given BODY section.
	 */
	public BODY fetchBody(int msgno, String section, int start, int size) throws ProtocolException {
		return fetchBody(msgno, section, start, size, false, null);
	}

	/**
	 * Partial FETCH of given BODY section, without setting SEEN flag.
	 */
	public BODY peekBody(int msgno, String section, int start, int size, ByteArray ba) throws ProtocolException {
		return fetchBody(msgno, section, start, size, true, ba);
	}

	/**
	 * Partial FETCH of given BODY section.
	 */
	public BODY fetchBody(int msgno, String section, int start, int size, ByteArray ba) throws ProtocolException {
		return fetchBody(msgno, section, start, size, false, ba);
	}

	protected BODY fetchBody(int msgno, String section, int start, int size, boolean peek, ByteArray ba) throws ProtocolException {
		this.ba = ba; // save for later use by getResponseBuffer
		Response[] r = fetch(msgno, (peek ? "BODY.PEEK[" : "BODY[") + (section == null ? "]<" : (section + "]<")) + String.valueOf(start) + "." + String.valueOf(size) + ">");

		notifyResponseHandlers(r);

		Response response = r[r.length - 1];
		if (response.isOK())
			return (BODY) FetchResponse.getItem(r, msgno, BODY.class);
		else if (response.isNO())
			return null;
		else {
			handleResult(response);
			return null;
		}
	}

	/**
	 * Return a buffer to read a response into.
	 * The buffer is provided by fetchBody and is
	 * used only once.
	 */
	protected ByteArray getResponseBuffer() {
		ByteArray ret = ba;
		ba = null;
		return ret;
	}

	/**
	 * Fetch the specified RFC822 Data item. 'what' names
	 * the item to be fetched. 'what' can be <code>null</code> to fetch the
	 * whole message.
	 */
	public RFC822DATA fetchRFC822(int msgno, String what) throws ProtocolException {
		Response[] r = fetch(msgno, what == null ? "RFC822" : "RFC822." + what);

		// dispatch untagged responses
		notifyResponseHandlers(r);

		Response response = r[r.length - 1];
		if (response.isOK())
			return (RFC822DATA) FetchResponse.getItem(r, msgno, RFC822DATA.class);
		else if (response.isNO())
			return null;
		else {
			handleResult(response);
			return null;
		}
	}

	/**
	 * Fetch the FLAGS for the given message.
	 */
	public Flags fetchFlags(int msgno) throws ProtocolException {
		Flags flags = null;
		Response[] r = fetch(msgno, "FLAGS");

		// Search for our FLAGS response
		for (int i = 0, len = r.length; i < len; i++) {
			if (r[i] == null || !(r[i] instanceof FetchResponse) || ((FetchResponse) r[i]).getNumber() != msgno)
				continue;

			FetchResponse fr = (FetchResponse) r[i];
			if ((flags = (Flags) fr.getItem(Flags.class)) != null) {
				r[i] = null; // remove this response
				break;
			}
		}

		// dispatch untagged responses
		notifyResponseHandlers(r);
		handleResult(r[r.length - 1]);
		return flags;
	}

	/**
	 * Fetch the IMAP UID for the given message.
	 */
	public UID fetchUID(int msgno) throws ProtocolException {
		Response[] r = fetch(msgno, "UID");

		// dispatch untagged responses
		notifyResponseHandlers(r);

		Response response = r[r.length - 1];
		if (response.isOK())
			return (UID) FetchResponse.getItem(r, msgno, UID.class);
		else if (response.isNO()) // XXX: Issue NOOP ?
			return null;
		else {
			handleResult(response);
			return null; // NOTREACHED
		}
	}

	/**
	 * Get the sequence number for the given UID. A UID object
	 * containing the sequence number is returned. If the given UID
	 * is invalid, <code>null</code> is returned.
	 */
	public UID fetchSequenceNumber(long uid) throws ProtocolException {
		UID u = null;
		Response[] r = fetch(String.valueOf(uid), "UID", true);

		for (int i = 0, len = r.length; i < len; i++) {
			if (r[i] == null || !(r[i] instanceof FetchResponse))
				continue;

			FetchResponse fr = (FetchResponse) r[i];
			if ((u = (UID) fr.getItem(UID.class)) != null) {
				if (u.uid == uid) // this is the one we want
					break;
				else
					u = null;
			}
		}

		notifyResponseHandlers(r);
		handleResult(r[r.length - 1]);
		return u;
	}

	/**
	 * Get the sequence numbers for UIDs ranging from start till end.
	 * UID objects that contain the sequence numbers are returned.
	 * If no UIDs in the given range are found, an empty array is returned.
	 */
	public UID[] fetchSequenceNumbers(long start, long end) throws ProtocolException {
		Response[] r = fetch(String.valueOf(start) + ":" + (end == UIDFolder.LASTUID ? "*" : String.valueOf(end)), "UID", true);

		UID u;
		Vector v = new Vector();
		for (int i = 0, len = r.length; i < len; i++) {
			if (r[i] == null || !(r[i] instanceof FetchResponse))
				continue;

			FetchResponse fr = (FetchResponse) r[i];
			if ((u = (UID) fr.getItem(UID.class)) != null)
				v.addElement(u);
		}

		notifyResponseHandlers(r);
		handleResult(r[r.length - 1]);

		UID[] ua = new UID[v.size()];
		v.copyInto(ua);
		return ua;
	}

	/**
	 * Get the sequence numbers for UIDs ranging from start till end.
	 * UID objects that contain the sequence numbers are returned.
	 * If no UIDs in the given range are found, an empty array is returned.
	 */
	public UID[] fetchSequenceNumbers(long[] uids) throws ProtocolException {
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < uids.length; i++) {
			if (i > 0)
				sb.append(",");
			sb.append(String.valueOf(uids[i]));
		}

		Response[] r = fetch(sb.toString(), "UID", true);

		UID u;
		Vector v = new Vector();
		for (int i = 0, len = r.length; i < len; i++) {
			if (r[i] == null || !(r[i] instanceof FetchResponse))
				continue;

			FetchResponse fr = (FetchResponse) r[i];
			if ((u = (UID) fr.getItem(UID.class)) != null)
				v.addElement(u);
		}

		notifyResponseHandlers(r);
		handleResult(r[r.length - 1]);

		UID[] ua = new UID[v.size()];
		v.copyInto(ua);
		return ua;
	}

	public Response[] fetch(MessageSet[] msgsets, String what) throws ProtocolException {
		return fetch(MessageSet.toString(msgsets), what, false);
	}

	public Response[] fetch(int start, int end, String what) throws ProtocolException {
		return fetch(String.valueOf(start) + ":" + String.valueOf(end), what, false);
	}

	public Response[] fetch(int msg, String what) throws ProtocolException {
		return fetch(String.valueOf(msg), what, false);
	}

	private Response[] fetch(String msgSequence, String what, boolean uid) throws ProtocolException {
		if (uid)
			return command("UID FETCH " + msgSequence + " (" + what + ")", null);
		else
			return command("FETCH " + msgSequence + " (" + what + ")", null);
	}

	/**
	 * COPY command.
	 */
	public void copy(MessageSet[] msgsets, String mbox) throws ProtocolException {
		copy(MessageSet.toString(msgsets), mbox);
	}

	public void copy(int start, int end, String mbox) throws ProtocolException {
		copy(String.valueOf(start) + ":" + String.valueOf(end), mbox);
	}

	private void copy(String msgSequence, String mbox) throws ProtocolException {
		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);

		Argument args = new Argument();
		args.writeAtom(msgSequence);
		args.writeString(mbox);

		simpleCommand("COPY", args);
	}
	
	public void uidcopy(long uid, String mbox) throws ProtocolException {
		uidcopy(String.valueOf(uid), mbox);
	}
	
	public void uidcopy(UIDSet[] uids, String mbox) throws ProtocolException {
		uidcopy(UIDSet.toString(uids), mbox);
	}
	
	public void uidcopy(int start, int end, String mbox) throws ProtocolException {
		uidcopy(String.valueOf(start) + ":" + String.valueOf(end), mbox);
	}
	
	private void uidcopy(String msgSequence, String mbox) throws ProtocolException {
		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);
		
		Argument args = new Argument();
		args.writeAtom(msgSequence);
		args.writeString(mbox);
		
		simpleCommand("UID COPY", args);
	}

	/**
	 * MOVE command.
	 */
	public void move(MessageSet[] msgsets, String mbox) throws ProtocolException {
		move(MessageSet.toString(msgsets), mbox);
	}

	public void move(int start, int end, String mbox) throws ProtocolException {
		move(String.valueOf(start) + ":" + String.valueOf(end), mbox);
	}

	private void move(String msgSequence, String mbox) throws ProtocolException {
		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);

		Argument args = new Argument();
		args.writeAtom(msgSequence);
		args.writeString(mbox);

		simpleCommand("MOVE", args);
	}

	public void storeFlags(MessageSet[] msgsets, Flags flags, boolean set) throws ProtocolException {
		storeFlags(MessageSet.toString(msgsets), flags, set);
	}

	public void storeFlags(int start, int end, Flags flags, boolean set) throws ProtocolException {
		storeFlags(String.valueOf(start) + ":" + String.valueOf(end), flags, set);
	}

	/**
	 * Set the specified flags on this message.
	 * <p>
	 */
	public void storeFlags(int msg, Flags flags, boolean set) throws ProtocolException {
		storeFlags(String.valueOf(msg), flags, set);
	}

	private void storeFlags(String msgset, Flags flags, boolean set) throws ProtocolException {
		Response[] r;
		if (set)
			r = command("STORE " + msgset + " +FLAGS " + createFlagList(flags), null);
		else
			r = command("STORE " + msgset + " -FLAGS " + createFlagList(flags), null);

		// Dispatch untagged responses
		notifyResponseHandlers(r);
		handleResult(r[r.length - 1]);
	}

	/**
	 * Creates an IMAP flag_list from the given Flags object.
	 */
	private String createFlagList(Flags flags) {
		StringBuffer sb = new StringBuffer();
		sb.append("("); // start of flag_list

		Flags.Flag[] sf = flags.getSystemFlags(); // get the system flags
		boolean first = true;
		for (int i = 0; i < sf.length; i++) {
			String s;
			Flags.Flag f = sf[i];
			if (f == Flags.Flag.ANSWERED)
				s = "\\Answered";
			else if (f == Flags.Flag.DELETED)
				s = "\\Deleted";
			else if (f == Flags.Flag.DRAFT)
				s = "\\Draft";
			else if (f == Flags.Flag.FLAGGED)
				s = "\\Flagged";
			else if (f == Flags.Flag.RECENT)
				s = "\\Recent";
			else if (f == Flags.Flag.SEEN)
				s = "\\Seen";
			else
				continue; // skip it
			if (first)
				first = false;
			else
				sb.append(' ');
			sb.append(s);
		}

		String[] uf = flags.getUserFlags(); // get the user flag strings
		for (int i = 0; i < uf.length; i++) {
			if (first)
				first = false;
			else
				sb.append(' ');
			sb.append(uf[i]);
		}

		sb.append(")"); // terminate flag_list
		return sb.toString();
	}

	/**
	 * Issue the given search criterion on the specified message sets.
	 * Returns array of matching sequence numbers. An empty array
	 * is returned if no matches are found.
	 * 
	 * @param msgsets
	 *            array of MessageSets
	 * @param term
	 *            SearchTerm
	 * @return array of matching sequence numbers.
	 */
	public int[] search(MessageSet[] msgsets, SearchTerm term) throws ProtocolException, SearchException {
		return search(MessageSet.toString(msgsets), term);
	}

	/**
	 * Issue the given search criterion on all messages in this folder.
	 * Returns array of matching sequence numbers. An empty array
	 * is returned if no matches are found.
	 * 
	 * @param term
	 *            SearchTerm
	 * @return array of matching sequence numbers.
	 */
	public int[] search(SearchTerm term) throws ProtocolException, SearchException {
		return search("ALL", term);
	}

	/*
	 * Apply the given SearchTerm on the specified sequence.
	 * Returns array of matching sequence numbers. Note that an empty
	 * array is returned for no matches.
	 */
	private int[] search(String msgSequence, SearchTerm term) throws ProtocolException, SearchException {
		// Check if the search "text" terms contain only ASCII chars
		if (getSearchSequence().isAscii(term)) {
			try {
				return issueSearch(msgSequence, term, null);
			} catch (IOException ioex) { /* will not happen */
			}
		}

		/*
		 * The search "text" terms do contain non-ASCII chars. We need to
		 * use SEARCH CHARSET <charset> ...
		 * The charsets we try to use are UTF-8 and the locale's
		 * default charset. If the server supports UTF-8, great,
		 * always use it. Else we try to use the default charset.
		 */

		// Cycle thru the list of charsets
		for (int i = 0; i < searchCharsets.length; i++) {
			if (searchCharsets[i] == null)
				continue;

			try {
				return issueSearch(msgSequence, term, searchCharsets[i]);
			} catch (CommandFailedException cfx) {
				/*
				 * Server returned NO. For now, I'll just assume that
				 * this indicates that this charset is unsupported.
				 * We can check the BADCHARSET response code once
				 * that's spec'd into the IMAP RFC ..
				 */
				searchCharsets[i] = null;
				continue;
			} catch (IOException ioex) {
				/* Charset conversion failed. Try the next one */
				continue;
			} catch (ProtocolException pex) {
				throw pex;
			} catch (SearchException sex) {
				throw sex;
			}
		}

		// No luck.
		throw new SearchException("Search failed");
	}

	/*
	 * Apply the given SearchTerm on the specified sequence, using the
	 * given charset. <p>
	 * Returns array of matching sequence numbers. Note that an empty
	 * array is returned for no matches.
	 */
	private int[] issueSearch(String msgSequence, SearchTerm term, String charset) throws ProtocolException, SearchException, IOException {

		// Generate a search-sequence with the given charset
		Argument args = getSearchSequence().generateSequence(term, charset == null ? null : MimeUtility.javaCharset(charset));
		args.writeAtom(msgSequence);

		Response[] r;

		if (charset == null) // text is all US-ASCII
			r = command("SEARCH", args);
		else
			r = command("SEARCH CHARSET " + charset, args);

		Response response = r[r.length - 1];
		int[] matches = null;

		// Grab all SEARCH responses
		if (response.isOK()) { // command succesful
			Vector v = new Vector();
			int num;
			for (int i = 0, len = r.length; i < len; i++) {
				if (!(r[i] instanceof IMAPResponse))
					continue;

				IMAPResponse ir = (IMAPResponse) r[i];
				// There *will* be one SEARCH response.
				if (ir.keyEquals("SEARCH")) {
					while ((num = ir.readNumber()) != -1)
						v.addElement(new Integer(num));
					r[i] = null;
				}
			}

			// Copy the vector into 'matches'
			int vsize = v.size();
			matches = new int[vsize];
			for (int i = 0; i < vsize; i++)
				matches[i] = ((Integer) v.elementAt(i)).intValue();
		}

		// dispatch remaining untagged responses
		notifyResponseHandlers(r);
		handleResult(response);
		return matches;
	}

	/**
	 * Get the SearchSequence object.
	 * The SearchSequence object instance is saved in the searchSequence
	 * field. Subclasses of IMAPProtocol may override this method to
	 * return a subclass of SearchSequence, in order to add support for
	 * product-specific search terms.
	 * 
	 * @since JavaMail 1.4.6
	 */
	protected SearchSequence getSearchSequence() {
		if (searchSequence == null)
			searchSequence = new SearchSequence();
		return searchSequence;
	}

	/**
	 * Sort messages in the folder according to the specified sort criteria.
	 * If the search term is not null, limit the sort to only the messages
	 * that match the search term.
	 * Returns an array of sorted sequence numbers. An empty array
	 * is returned if no matches are found.
	 * 
	 * @param term
	 *            sort criteria
	 * @param sterm
	 *            SearchTerm
	 * @return array of matching sequence numbers.
	 * 
	 * @see "RFC 5256"
	 * @since JavaMail 1.4.4
	 */
	public int[] sort(SortTerm[] term, SearchTerm sterm) throws ProtocolException, SearchException {
		if (!hasCapability("SORT*"))
			throw new BadCommandException("SORT not supported");

		if (term == null || term.length == 0)
			throw new BadCommandException("Must have at least one sort term");

		Argument args = new Argument();
		Argument sargs = new Argument();
		for (int i = 0; i < term.length; i++)
			sargs.writeAtom(term[i].toString());
		args.writeArgument(sargs); // sort criteria

		args.writeAtom("UTF-8"); // charset specification
		if (sterm != null) {
			try {
				args.append(getSearchSequence().generateSequence(sterm, "UTF-8"));
			} catch (IOException ioex) {
				// should never happen
				throw new SearchException(ioex.toString());
			}
		} else
			args.writeAtom("ALL");

		Response[] r = command("SORT", args);
		Response response = r[r.length - 1];
		int[] matches = null;

		// Grab all SORT responses
		if (response.isOK()) { // command succesful
			Vector v = new Vector();
			int num;
			for (int i = 0, len = r.length; i < len; i++) {
				if (!(r[i] instanceof IMAPResponse))
					continue;

				IMAPResponse ir = (IMAPResponse) r[i];
				if (ir.keyEquals("SORT")) {
					while ((num = ir.readNumber()) != -1)
						v.addElement(new Integer(num));
					r[i] = null;
				}
			}

			// Copy the vector into 'matches'
			int vsize = v.size();
			matches = new int[vsize];
			for (int i = 0; i < vsize; i++)
				matches[i] = ((Integer) v.elementAt(i)).intValue();
		}

		// dispatch remaining untagged responses
		notifyResponseHandlers(r);
		handleResult(response);
		return matches;
	}

	/**
	 * NAMESPACE Command.
	 * 
	 * @see "RFC2342"
	 */
	public Namespaces namespace() throws ProtocolException {
		if (!hasCapability("NAMESPACE"))
			throw new BadCommandException("NAMESPACE not supported");

		Response[] r = command("NAMESPACE", null);

		Namespaces namespace = null;
		Response response = r[r.length - 1];

		// Grab NAMESPACE response
		if (response.isOK()) { // command succesful
			for (int i = 0, len = r.length; i < len; i++) {
				if (!(r[i] instanceof IMAPResponse))
					continue;

				IMAPResponse ir = (IMAPResponse) r[i];
				if (ir.keyEquals("NAMESPACE")) {
					if (namespace == null)
						namespace = new Namespaces(ir);
					r[i] = null;
				}
			}
		}

		// dispatch remaining untagged responses
		notifyResponseHandlers(r);
		handleResult(response);
		return namespace;
	}

	/**
	 * GETQUOTAROOT Command.
	 * 
	 * Returns an array of Quota objects, representing the quotas
	 * for this mailbox and, indirectly, the quotaroots for this
	 * mailbox.
	 * 
	 * @see "RFC2087"
	 */
	public Quota[] getQuotaRoot(String mbox) throws ProtocolException {
		if (!hasCapability("QUOTA"))
			throw new BadCommandException("GETQUOTAROOT not supported");

		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);

		Argument args = new Argument();
		args.writeString(mbox);

		Response[] r = command("GETQUOTAROOT", args);

		Response response = r[r.length - 1];

		Hashtable tab = new Hashtable();

		// Grab all QUOTAROOT and QUOTA responses
		if (response.isOK()) { // command succesful
			for (int i = 0, len = r.length; i < len; i++) {
				if (!(r[i] instanceof IMAPResponse))
					continue;

				IMAPResponse ir = (IMAPResponse) r[i];
				if (ir.keyEquals("QUOTAROOT")) {
					// quotaroot_response
					// ::= "QUOTAROOT" SP astring *(SP astring)

					// read name of mailbox and throw away
					ir.readAtomString();
					// for each quotaroot add a placeholder quota
					String root = null;
					while ((root = ir.readAtomString()) != null && root.length() > 0)
						tab.put(root, new Quota(root));
					r[i] = null;
				} else if (ir.keyEquals("QUOTA")) {
					Quota quota = parseQuota(ir);
					Quota q = (Quota) tab.get(quota.quotaRoot);
					if (q != null && q.resources != null) {
						// XXX - should merge resources
					}
					tab.put(quota.quotaRoot, quota);
					r[i] = null;
				}
			}
		}

		// dispatch remaining untagged responses
		notifyResponseHandlers(r);
		handleResult(response);

		Quota[] qa = new Quota[tab.size()];
		Enumeration e = tab.elements();
		for (int i = 0; e.hasMoreElements(); i++)
			qa[i] = (Quota) e.nextElement();
		return qa;
	}

	/**
	 * GETQUOTA Command.
	 * 
	 * Returns an array of Quota objects, representing the quotas
	 * for this quotaroot.
	 * 
	 * @see "RFC2087"
	 */
	public Quota[] getQuota(String root) throws ProtocolException {
		if (!hasCapability("QUOTA"))
			throw new BadCommandException("QUOTA not supported");

		Argument args = new Argument();
		args.writeString(root);

		Response[] r = command("GETQUOTA", args);

		Quota quota = null;
		Vector v = new Vector();
		Response response = r[r.length - 1];

		// Grab all QUOTA responses
		if (response.isOK()) { // command succesful
			for (int i = 0, len = r.length; i < len; i++) {
				if (!(r[i] instanceof IMAPResponse))
					continue;

				IMAPResponse ir = (IMAPResponse) r[i];
				if (ir.keyEquals("QUOTA")) {
					quota = parseQuota(ir);
					v.addElement(quota);
					r[i] = null;
				}
			}
		}

		// dispatch remaining untagged responses
		notifyResponseHandlers(r);
		handleResult(response);
		Quota[] qa = new Quota[v.size()];
		v.copyInto(qa);
		return qa;
	}

	/**
	 * SETQUOTA Command.
	 * 
	 * Set the indicated quota on the corresponding quotaroot.
	 * 
	 * @see "RFC2087"
	 */
	public void setQuota(Quota quota) throws ProtocolException {
		if (!hasCapability("QUOTA"))
			throw new BadCommandException("QUOTA not supported");

		Argument args = new Argument();
		args.writeString(quota.quotaRoot);
		Argument qargs = new Argument();
		if (quota.resources != null) {
			for (int i = 0; i < quota.resources.length; i++) {
				qargs.writeAtom(quota.resources[i].name);
				qargs.writeNumber(quota.resources[i].limit);
			}
		}
		args.writeArgument(qargs);

		Response[] r = command("SETQUOTA", args);
		Response response = r[r.length - 1];

		// XXX - It's not clear from the RFC whether the SETQUOTA command
		// will provoke untagged QUOTA responses. If it does, perhaps
		// we should grab them here and return them?

		/*
		 * Quota quota = null;
		 * Vector v = new Vector();
		 * 
		 * // Grab all QUOTA responses
		 * if (response.isOK()) { // command succesful
		 * for (int i = 0, len = r.length; i < len; i++) {
		 * if (!(r[i] instanceof IMAPResponse))
		 * continue;
		 * 
		 * IMAPResponse ir = (IMAPResponse)r[i];
		 * if (ir.keyEquals("QUOTA")) {
		 * quota = parseQuota(ir);
		 * v.addElement(quota);
		 * r[i] = null;
		 * }
		 * }
		 * }
		 */

		// dispatch remaining untagged responses
		notifyResponseHandlers(r);
		handleResult(response);
		/*
		 * Quota[] qa = new Quota[v.size()];
		 * v.copyInto(qa);
		 * return qa;
		 */
	}

	/**
	 * Parse a QUOTA response.
	 */
	private Quota parseQuota(Response r) throws ParsingException {
		// quota_response ::= "QUOTA" SP astring SP quota_list
		String quotaRoot = r.readAtomString(); // quotaroot ::= astring
		Quota q = new Quota(quotaRoot);
		r.skipSpaces();
		// quota_list ::= "(" #quota_resource ")"
		if (r.readByte() != '(')
			throw new ParsingException("parse error in QUOTA");

		Vector v = new Vector();
		while (r.peekByte() != ')') {
			// quota_resource ::= atom SP number SP number
			String name = r.readAtom();
			if (name != null) {
				long usage = r.readLong();
				long limit = r.readLong();
				Quota.Resource res = new Quota.Resource(name, usage, limit);
				v.addElement(res);
			}
		}
		r.readByte();
		q.resources = new Quota.Resource[v.size()];
		v.copyInto(q.resources);
		return q;
	}

	/**
	 * SETACL Command.
	 * 
	 * @see "RFC2086"
	 */
	public void setACL(String mbox, char modifier, ACL acl) throws ProtocolException {
		if (!hasCapability("ACL"))
			throw new BadCommandException("ACL not supported");

		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);

		Argument args = new Argument();
		args.writeString(mbox);
		args.writeString(acl.getName());
		String rights = acl.getRights().toString();
		if (modifier == '+' || modifier == '-')
			rights = modifier + rights;
		args.writeString(rights);

		Response[] r = command("SETACL", args);
		Response response = r[r.length - 1];

		// dispatch untagged responses
		notifyResponseHandlers(r);
		handleResult(response);
	}

	/**
	 * DELETEACL Command.
	 * 
	 * @see "RFC2086"
	 */
	public void deleteACL(String mbox, String user) throws ProtocolException {
		if (!hasCapability("ACL"))
			throw new BadCommandException("ACL not supported");

		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);

		Argument args = new Argument();
		args.writeString(mbox);
		args.writeString(user);

		Response[] r = command("DELETEACL", args);
		Response response = r[r.length - 1];

		// dispatch untagged responses
		notifyResponseHandlers(r);
		handleResult(response);
	}

	/**
	 * GETACL Command.
	 * 
	 * @see "RFC2086"
	 */
	public ACL[] getACL(String mbox) throws ProtocolException {
		if (!hasCapability("ACL"))
			throw new BadCommandException("ACL not supported");

		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);

		Argument args = new Argument();
		args.writeString(mbox);

		Response[] r = command("GETACL", args);
		Response response = r[r.length - 1];

		// Grab all ACL responses
		Vector v = new Vector();
		if (response.isOK()) { // command succesful
			for (int i = 0, len = r.length; i < len; i++) {
				if (!(r[i] instanceof IMAPResponse))
					continue;

				IMAPResponse ir = (IMAPResponse) r[i];
				if (ir.keyEquals("ACL")) {
					// acl_data ::= "ACL" SPACE mailbox
					// *(SPACE identifier SPACE rights)
					// read name of mailbox and throw away
					ir.readAtomString();
					String name = null;
					while ((name = ir.readAtomString()) != null) {
						String rights = ir.readAtomString();
						if (rights == null)
							break;
						ACL acl = new ACL(name, new Rights(rights));
						v.addElement(acl);
					}
					r[i] = null;
				}
			}
		}

		// dispatch remaining untagged responses
		notifyResponseHandlers(r);
		handleResult(response);
		ACL[] aa = new ACL[v.size()];
		v.copyInto(aa);
		return aa;
	}

	/**
	 * LISTRIGHTS Command.
	 * 
	 * @see "RFC2086"
	 */
	public Rights[] listRights(String mbox, String user) throws ProtocolException {
		if (!hasCapability("ACL"))
			throw new BadCommandException("ACL not supported");

		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);

		Argument args = new Argument();
		args.writeString(mbox);
		args.writeString(user);

		Response[] r = command("LISTRIGHTS", args);
		Response response = r[r.length - 1];

		// Grab LISTRIGHTS response
		Vector v = new Vector();
		if (response.isOK()) { // command succesful
			for (int i = 0, len = r.length; i < len; i++) {
				if (!(r[i] instanceof IMAPResponse))
					continue;

				IMAPResponse ir = (IMAPResponse) r[i];
				if (ir.keyEquals("LISTRIGHTS")) {
					// listrights_data ::= "LISTRIGHTS" SPACE mailbox
					// SPACE identifier SPACE rights *(SPACE rights)
					// read name of mailbox and throw away
					ir.readAtomString();
					// read identifier and throw away
					ir.readAtomString();
					String rights;
					while ((rights = ir.readAtomString()) != null)
						v.addElement(new Rights(rights));
					r[i] = null;
				}
			}
		}

		// dispatch remaining untagged responses
		notifyResponseHandlers(r);
		handleResult(response);
		Rights[] ra = new Rights[v.size()];
		v.copyInto(ra);
		return ra;
	}

	/**
	 * MYRIGHTS Command.
	 * 
	 * @see "RFC2086"
	 */
	public Rights myRights(String mbox) throws ProtocolException {
		if (!hasCapability("ACL"))
			throw new BadCommandException("ACL not supported");

		// encode the mbox as per RFC2060
		mbox = BASE64MailboxEncoder.encode(mbox);

		Argument args = new Argument();
		args.writeString(mbox);

		Response[] r = command("MYRIGHTS", args);
		Response response = r[r.length - 1];

		// Grab MYRIGHTS response
		Rights rights = null;
		if (response.isOK()) { // command succesful
			for (int i = 0, len = r.length; i < len; i++) {
				if (!(r[i] instanceof IMAPResponse))
					continue;

				IMAPResponse ir = (IMAPResponse) r[i];
				if (ir.keyEquals("MYRIGHTS")) {
					// myrights_data ::= "MYRIGHTS" SPACE mailbox SPACE rights
					// read name of mailbox and throw away
					ir.readAtomString();
					String rs = ir.readAtomString();
					if (rights == null)
						rights = new Rights(rs);
					r[i] = null;
				}
			}
		}

		// dispatch remaining untagged responses
		notifyResponseHandlers(r);
		handleResult(response);
		return rights;
	}

	/*
	 * The tag used on the IDLE command. Set by idleStart() and
	 * used in processIdleResponse() to determine if the response
	 * is the matching end tag.
	 */
	private volatile String idleTag;

	/**
	 * IDLE Command.
	 * <p>
	 * 
	 * If the server supports the IDLE command extension, the IDLE command is
	 * issued and this method blocks until a response has been received. Once
	 * the first response has been received, the IDLE command is terminated and
	 * all responses are collected and handled and this method returns.
	 * <p>
	 * 
	 * Note that while this method is blocked waiting for a response, no other
	 * threads may issue any commands to the server that would use this same
	 * connection.
	 * 
	 * @see "RFC2177"
	 * @since JavaMail 1.4.1
	 */
	public synchronized void idleStart() throws ProtocolException {
		if (!hasCapability("IDLE"))
			throw new BadCommandException("IDLE not supported");

		Vector v = new Vector();
		boolean done = false;
		Response r = null;

		// write the command
		try {
			idleTag = writeCommand("IDLE", null);
		} catch (LiteralException lex) {
			v.addElement(lex.getResponse());
			done = true;
		} catch (Exception ex) {
			// Convert this into a BYE response
			v.addElement(Response.byeResponse(ex));
			done = true;
		}

		while (!done) {
			try {
				r = readResponse();
			} catch (IOException ioex) {
				// convert this into a BYE response
				r = Response.byeResponse(ioex);
			} catch (ProtocolException pex) {
				continue; // skip this response
			}

			v.addElement(r);

			if (r.isContinuation() || r.isBYE())
				done = true;
		}

		Response[] responses = new Response[v.size()];
		v.copyInto(responses);
		r = responses[responses.length - 1];

		// dispatch remaining untagged responses
		notifyResponseHandlers(responses);
		if (!r.isContinuation())
			handleResult(r);
	}

	/**
	 * While an IDLE command is in progress, read a response
	 * sent from the server. The response is read with no locks
	 * held so that when the read blocks waiting for the response
	 * from the server it's not holding locks that would prevent
	 * other threads from interrupting the IDLE command.
	 * 
	 * @since JavaMail 1.4.1
	 */
	public synchronized Response readIdleResponse() {
		if (idleTag == null)
			return null; // IDLE not in progress
		Response r = null;
		while (r == null) {
			try {
				r = readResponse();
			} catch (InterruptedIOException iioex) {
				/*
				 * If a socket timeout was set, the read will timeout
				 * before the IDLE times out. In that case, just go
				 * back and read some more. After all, the point of
				 * IDLE is to sit here and wait until something happens.
				 */
				if (iioex.bytesTransferred == 0)
					r = null; // keep trying
				else
					// convert this into a BYE response
					r = Response.byeResponse(iioex);
			} catch (IOException ioex) {
				// convert this into a BYE response
				r = Response.byeResponse(ioex);
			} catch (ProtocolException pex) {
				// convert this into a BYE response
				r = Response.byeResponse(pex);
			}
		}
		return r;
	}

	/**
	 * Process a response returned by readIdleResponse().
	 * This method will be called with appropriate locks
	 * held so that the processing of the response is safe.
	 * 
	 * @since JavaMail 1.4.1
	 */
	public boolean processIdleResponse(Response r) throws ProtocolException {
		Response[] responses = new Response[1];
		responses[0] = r;
		boolean done = false; // done reading responses?
		notifyResponseHandlers(responses);

		if (r.isBYE()) // shouldn't wait for command completion response
			done = true;

		// If this is a matching command completion response, we are done
		if (r.isTagged() && r.getTag().equals(idleTag))
			done = true;

		if (done)
			idleTag = null; // no longer in IDLE

		handleResult(r);
		return !done;
	}

	// the DONE command to break out of IDLE
	private static final byte[] DONE = { 'D', 'O', 'N', 'E', '\r', '\n' };

	/**
	 * Abort an IDLE command. While one thread is blocked in
	 * readIdleResponse(), another thread will use this method
	 * to abort the IDLE command, which will cause the server
	 * to send the closing tag for the IDLE command, which
	 * readIdleResponse() and processIdleResponse() will see
	 * and terminate the IDLE state.
	 * 
	 * @since JavaMail 1.4.1
	 */
	public void idleAbort() throws ProtocolException {
		OutputStream os = getOutputStream();
		try {
			os.write(DONE);
			os.flush();
		} catch (IOException ex) {
			// nothing to do, hope to detect it again later
		}
	}
}
